package com.syyo.gulimall.product.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.syyo.common.enums.ResultEnum;
import com.syyo.common.exception.SysException;
import com.syyo.common.utils.MyListUtils;
import com.syyo.common.utils.MyStringUtils;
import com.syyo.gulimall.product.domain.entity.CategoryEntity;
import com.syyo.gulimall.product.domain.req.CategoryReq;
import com.syyo.gulimall.product.domain.vo.Catelog2Vo;
import com.syyo.gulimall.product.mapper.CategoryMapper;
import com.syyo.gulimall.product.service.CategoryService;
import com.syyo.gulimall.product.utils.TreeUtils;
import org.apache.commons.lang3.StringUtils;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;


/**
 * 商品三级分类
 *
 * @author syyo
 * @email syyo@gmail.com
 * @date 2021-01-20 09:32:12
 */
@Service
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, CategoryEntity> implements CategoryService {

    @Autowired
    private CategoryMapper categoryMapper;

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private RedissonClient redissonClient;

    /**
     * 新增
     */
    @Override
    @Transactional
    public int add(CategoryReq req) {
        CategoryEntity category = new CategoryEntity();
        category.setName(req.getName());
        List<Long> pidList = req.getPidList();
        category.setCatLevel(MyStringUtils.getRank(pidList));
        category.setParentCid(MyStringUtils.getPid(pidList));
        category.setShowStatus(1);
        category.setSort(req.getSort());
        // 添加创建时间和更新时间
        int insert = categoryMapper.insert(category);
        if (insert != 1) {
            throw new SysException(ResultEnum.E_10001.getCode(), ResultEnum.E_10001.getMessage());
        }
        return insert;
    }

    /**
     * 删除
     */
    @Override
    @Transactional
    public int del(Integer id) {
        int insert = categoryMapper.deleteById(id);
        if (insert != 1) {
            throw new SysException(ResultEnum.E_10002.getCode(), ResultEnum.E_10002.getMessage());
        }
        return insert;
    }

    /**
     * 编辑
     */
    @Override
    @Transactional
    public int edit(CategoryReq req) {
        CategoryEntity category = new CategoryEntity();
        // 添加更新时间
        category.setCatId(req.getCatId());
        category.setName(req.getName());
        int insert = categoryMapper.updateById(category);
        if (insert != 1) {
            throw new SysException(ResultEnum.E_10003.getCode(), ResultEnum.E_10003.getMessage());
        }
        return insert;
    }

    /**
     * 详情
     */
    @Override
    public CategoryEntity findOne(Integer id) {
        return categoryMapper.selectById(id);
    }

    /**
     * 列表
     */
    @Override
    public IPage<CategoryEntity> findAll(Integer pageNum, Integer pageSize, CategoryReq req) {
        Page<CategoryEntity> teacherPage = new Page<CategoryEntity>(pageNum, pageSize);
        QueryWrapper<CategoryEntity> wrapper = new QueryWrapper<>();
        return categoryMapper.selectPage(teacherPage, wrapper);
    }

    @Override
    public List<CategoryEntity> tree() {
        List<CategoryEntity> categoryEntities = categoryMapper.selectList(null);
        return TreeUtils.listToCategoryTree(categoryEntities);
    }

    @Cacheable("product") // 当前方法的结果需要缓存
    @Override
    public List<CategoryEntity> getLevel1Categorys() {
        return categoryMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
    }

    /**
     * 优化这个接口
     * 将所有的查询改成一次查询
     * 然后在内存里进行计算处理
     * <p>
     * todo 因为lettuce的问题产生堆外内容溢出 OutOfDirectMemoryError
     * springboot 2.0以后默认使用lettuce作为操作redis的客户端，它使用netty进行网络通信
     * lettuce的bug导出netty堆外内容溢出，netty如果没有指定堆外对内会默认使用jvm的配置内存
     * 解决：
     * 1、升级lettuce客户端
     * 2、切换使用jedis
     * <p>
     * <p>
     * 缓存3大问题
     * 缓存穿透：查一个空的值，解决：把空值存入缓存里
     * 缓存雪崩：多个key同时过期，解决：多个key随机设置过期时间
     * 缓存击穿：热点key过期，解决：加锁
     */
    @Override
    public Map<String, List<Catelog2Vo>> getCatelogJson() {
        ValueOperations<String, String> redis = redisTemplate.opsForValue();
        String catalogJSON = redis.get("catalogJSON");
        if (StringUtils.isEmpty(catalogJSON)) {

            // 本地锁
//            Map<String, List<Catelog2Vo>> catelogJsonDb = getCatelogJsonDbLocalLock();
            // redis分布式锁
//            Map<String, List<Catelog2Vo>> catelogJsonDb = getCatelogJsonDbRedisLock();
            Map<String, List<Catelog2Vo>> catelogJsonDb = getCatelogJsonDbRedissonLock();

            return catelogJsonDb;
        }
        System.out.println("缓存命中...直接返回");
        Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {
        });
        // 从数据库查询
        return result;
    }


    public List<CategoryEntity> getPid(List<CategoryEntity> list, Long pid) {
        return list.stream().filter(item -> item.getParentCid() == pid).collect(Collectors.toList());
    }


    /**
     * 使用redisson分布式锁
     * <p>
     * 缓存里的数据怎么和数据库保存一致
     * 1）双写模式
     * 2）失效模式
     */
    public Map<String, List<Catelog2Vo>> getCatelogJsonDbRedissonLock() {

        /**
         * 1.锁的名字，锁的粒度越细，速度越快
         * 具体缓存的某个数据，商品  ，product-lock-11
         *
         */
        RLock lock = redissonClient.getLock("catalogJSON-lock");
        lock.lock();
        Map<String, List<Catelog2Vo>> dataFromDb;
        try {
            dataFromDb = getDataFromDb();
        } finally {
            lock.unlock();
        }
        return dataFromDb;

    }


    /**
     * 分布式锁
     * 使用分布式锁
     */
    public Map<String, List<Catelog2Vo>> getCatelogJsonDbRedisLock() {

        /**
         * 去redis占坑
         *
         *
         */
        String uuid = UUID.randomUUID().toString();
        // 设置过期时间必须和加锁是原子操作
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS);

        if (lock) {
            System.out.println("获取分布式锁成功");
            // 加锁成功
            Map<String, List<Catelog2Vo>> dataFromDb;
            try {
                dataFromDb = getDataFromDb();
            } finally {
                // 使用lua脚本解锁
                String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
                // 原子删锁
                Long lock1 = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);
            }

            // 必须原子操作
           /* String redisUuid = redisTemplate.opsForValue().get("lock");
            if (uuid.equals(redisUuid)){
                redisTemplate.delete("lock");
            }*/
            return dataFromDb;
        } else {
            // 加锁失败，重试
            System.out.println("获取分布式锁失败...等待重试");
            try {
                Thread.sleep(200);
            } catch (Exception e) {
                e.printStackTrace();
            }
            // 休眠100毫秒
            return getCatelogJsonDbRedisLock();
        }
    }

    private Map<String, List<Catelog2Vo>> getDataFromDb() {
        String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
        if (!StringUtils.isEmpty(catalogJSON)) {
            // 如果缓存不为空直接返回
            Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {
            });
            // 从数据库查询
            return result;
        }
        System.out.println("查询了数据库: " + Thread.currentThread().getId());
        List<CategoryEntity> list = categoryMapper.selectList(null);

        List<CategoryEntity> level1Categorys = getPid(list, 0L);
        Map<String, List<Catelog2Vo>> parent_cid = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
            // 1.拿到每一个1级分类
            List<CategoryEntity> categoryEntities = getPid(list, v.getCatId());
            List<Catelog2Vo> catelog2Vos = null;
            if (MyListUtils.isNotEmpty(categoryEntities)) {
                catelog2Vos = categoryEntities.stream().map(l2 -> {
                    Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());
                    // 查找二级分类下的三级分类
                    List<CategoryEntity> level3 = getPid(list, l2.getCatId());
                    if (MyListUtils.isNotEmpty(level3)) {
                        //封装成指定的格式
                        List<Catelog2Vo.Catelog3Vo> l3List = level3.stream().map(l3 -> {
                            Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
                            return catelog3Vo;
                        }).collect(Collectors.toList());
                        catelog2Vo.setCatalog3List(l3List);
                    }
                    return catelog2Vo;
                }).collect(Collectors.toList());
            }
            return catelog2Vos;
        }));
        String s = JSON.toJSONString(parent_cid);
        redisTemplate.opsForValue().set("catalogJSON", s, 1, TimeUnit.DAYS);
        return parent_cid;
    }

    /**
     * 使用本地锁
     *
     * @return
     */
    public Map<String, List<Catelog2Vo>> getCatelogJsonDbLocalLock() {

        /**
         * 1.synchronized (this),springboot 所有组件在容器里是单列的，
         * 本地锁只能锁当前进程，优点速度快
         *
         */
        synchronized (this) {
            return getDataFromDb();
        }

    }
}